﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
using System.Text;
using System.Web.Routing;

namespace Cintellect.Web.Mvc.Sitemap
{
	public static class SitemapFactory
	{

		public static string Create()
		{
			Assembly assembly = Assembly.GetCallingAssembly();
			return GetSitemap(assembly, null);
		}

		public static string Create(Dictionary<string, Func<Url[]>> queries)
		{
			Assembly assembly = Assembly.GetCallingAssembly();
			return GetSitemap(assembly, queries);
		}

		private static string GetSitemap(Assembly assembly, Dictionary<string, Func<Url[]>> queries)
		{
			Urlset urlset = new Urlset();
			SitemapAttribute sitemap;

			string baseUrl = String.Format("{0}://{1}/", HttpContext.Current.Request.Url.Scheme, HttpContext.Current.Request.Url.Authority);
			bool hasAuthorizeAttribute = false;
			int indexOf = -1;
			string controller = String.Empty;
			string location = String.Empty;
			string route = String.Empty;

			// Get all the controller classes in the calling assembly.
			
			var types = assembly.GetTypes().Where(t => t.IsClass == true && t.BaseType == typeof(System.Web.Mvc.Controller));

			// I'm sure a lot of what follows can be cleaned up using LINQ - I'm just trying to get this working for right now.
			foreach (Type type in types)
			{

				// Reinitialize indexOf, location, and route so we're certain the previous
				// values won't contaminate our url.
				indexOf = -1;
				controller = type.Name.ToLower().Replace("controller", "");
				location = String.Empty;
				route = String.Empty;

				MethodInfo[] methods = type.GetMethods();

				// Loop through all the methods picking out the controller actions.
				foreach (MethodInfo method in methods)
				{
					hasAuthorizeAttribute = false;
					sitemap = null;

					// Inspect the attributes on each actions.
					foreach (Attribute attribute in Attribute.GetCustomAttributes(method))
					{
						if (attribute.GetType().Equals(typeof(AuthorizeAttribute)))
						{
							hasAuthorizeAttribute = true;
						}
						else if (attribute.GetType().Equals(typeof(SitemapAttribute)))
						{
							sitemap = (SitemapAttribute)attribute;
						}
					}

					// Only construct a Url object if a Sitemap attribute was found on the action
					// and there was either no Authorize attribute or we're ignoring Authorize attributes.
					if (sitemap != null && (!hasAuthorizeAttribute || sitemap.IgnoreAuthorizeAttribute))
					{
						Url currentUrl = new Url();

						location = String.Empty;

						// Get the route from the RouteTable and construct url location by replacing
						// the {controller} and {action} placeholders with the current controller and
						// action names. If a route wasn't specified, we'll assume the default route is
						// {controller}/{action} which will be appended onto the end of the base url.
						if (!String.IsNullOrEmpty(sitemap.Route))
						{
							route = ((Route)RouteTable.Routes[sitemap.Route]).Url.ToLower();

							// Controller.
							location = route.Replace("{controller}", controller);

							// Action.
							location = location.Replace("{action}", method.Name);

							indexOf = location.IndexOf("{");

							if (indexOf > -1)
							{
								location = location.Substring(0, indexOf - 1);
							}
						}
						else
						{
							location = String.Format("{0}/{1}", controller, method.Name).Trim('/');
						}

						currentUrl.Location = String.Format("{0}{1}", baseUrl, location);
						currentUrl.ChangeFrequency = sitemap.ChangeFrequency;
						if (sitemap.LastModified != DateTime.MinValue)
						{
							currentUrl.LastModified = sitemap.LastModified;
						}
						if (sitemap.Priority != 0.5f)
						{
							currentUrl.Priority = sitemap.Priority;
						}

						urlset.Urls.Add(currentUrl);

						// If the current Action's Sitemap attribute provided a QueryKey to 
						// generate extended sitemap locations, invoke the method and add the
						// Urls to the list.
						if (queries != null && !String.IsNullOrEmpty(sitemap.QueryKey))
						{
							Func<Url[]> query = queries[sitemap.QueryKey];

							if (query == null)
							{
								throw new NullReferenceException();
							}

							foreach (Url url in query.Invoke())
							{
								urlset.Urls.Add(url);
							}
						}
					}
				}
			}

			// Finally, serialize and write it out to a UTF-8 encoded string so it can
			// be rendered to the search bot.
			MemoryStream memory = new MemoryStream();
			XmlTextWriter xmlWriter = new XmlTextWriter(memory, Encoding.UTF8);

			XmlSerializerNamespaces namespaces = new XmlSerializerNamespaces();
			namespaces.Add(String.Empty, "http://www.sitemaps.org/schemas/sitemap/0.9");

			XmlSerializer serializer = new XmlSerializer(typeof(Urlset), "http://www.sitemaps.org/schemas/sitemap/0.9");
			serializer.Serialize(xmlWriter, urlset, namespaces);

			return Encoding.UTF8.GetString(memory.ToArray());
		}

	}
}
